﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Microsoft.SharePoint.Administration;

namespace Mystery.SharePoint
{
	/// <summary>
	/// Provides easy access to making changes to the web.config file
	/// </summary>
	public class WebConfigModifications 
	{
		private SPWebApplication _application;
		private string _owner;
		List<SPWebConfigModification> _existingModifications = new List<SPWebConfigModification>();
		List<SPWebConfigModification> _newModifications = new List<SPWebConfigModification>();

		/// <summary>
		/// Constructs the Web.config modification object
		/// </summary>
		/// <param name="application">The web application that owns the web.config</param>
		/// <param name="owner">The owner of these modifications. i.e. the name of the feature registering them</param>
		public WebConfigModifications(SPWebApplication application, string owner)
		{
			if (application == null) throw new ArgumentNullException("application");
			if (string.IsNullOrEmpty(owner) == true) throw new ArgumentNullException("owner");

			_application = application;
			_owner = owner;

			// find all our existing modifications so that we can easily compare it agains the new ones
			foreach (SPWebConfigModification modification in application.WebConfigModifications)
			{
				if (modification.Owner == _owner)
				{
					_existingModifications.Add(modification);
				}
			}
		}
		/// <summary>
		/// Adds a child node modification to the list of modifications that will be applied
		/// </summary>
		/// <param name="name">Name as used by SPWebConfigModification</param>
		/// <param name="path">Xpath as used by SPWebConfigModification</param>
		/// <param name="value">Value as used by SPWebConfigModification</param>
		public void AddChildNode(string name, string path, string value)
		{
			if (string.IsNullOrEmpty(name) == true) throw new ArgumentNullException("name");
			if (string.IsNullOrEmpty(path) == true) throw new ArgumentNullException("path");
			if (string.IsNullOrEmpty(value) == true) throw new ArgumentNullException("value");

			Logger.L.AddInformation(Category.Configuration, "Adding ChildNode: {0} xpath:{1} value:{2}", name, path, value);

			// create a new modification
			SPWebConfigModification modification = new SPWebConfigModification(name, path);
			modification.Owner = _owner;
			modification.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode;
			modification.Sequence = 0;
			modification.Value = value;
			_newModifications.Add(modification);
		}
		/// <summary>
		/// Adds an attribute modification to the list of modifications that will be applied
		/// </summary>
		/// <param name="name">Name as used by SPWebConfigModification</param>
		/// <param name="path">Xpath as used by SPWebConfigModification</param>
		/// <param name="value">Value as used by SPWebConfigModification</param>
		public void AddAttribute(string name, string path, string value)
		{
			if (string.IsNullOrEmpty(name) == true) throw new ArgumentNullException("name");
			if (string.IsNullOrEmpty(path) == true) throw new ArgumentNullException("path");
			if (string.IsNullOrEmpty(value) == true) throw new ArgumentNullException("value");

			Logger.L.AddInformation(Category.Configuration, "Adding attribute: {0} xpath:{1} value:{2}", name, path, value);

			SPWebConfigModification modification = new SPWebConfigModification(name, path);
			modification.Owner = _owner;
			modification.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureAttribute;
			modification.Sequence = 0;
			modification.Value = value;
			_newModifications.Add(modification);
		}
		/// <summary>
		/// Adds an HTTP module entry to the list of modifications that will be applied
		/// </summary>
		/// <param name="name">Name of module</param>
		/// <param name="implementingType">Type that implements the module</param>
		public void AddHttpModule(string name, Type implementingType)
		{
			if (string.IsNullOrEmpty(name) == true) throw new ArgumentNullException("name");
			if (implementingType == null) throw new ArgumentNullException("implementingType");

			Logger.L.AddInformation(Category.Configuration, "Adding HttpModule: {0}", name);

			string n = string.Format(null, "add[@name='{0}'][@type='{1}']", name, implementingType.AssemblyQualifiedName); 
			string x = "configuration/system.web/httpModules";
			string v = string.Format(null, "<add name=\"{0}\" type=\"{1}\" />", name, implementingType.AssemblyQualifiedName);

			AddChildNode(n, x, v);
		}
		/// <summary>
		/// Adds an HTTP handler entry to the list of modifications that will be applied
		/// </summary>
		/// <param name="path">Handler path</param>
		/// <param name="verb">Handler verbs</param>
		/// <param name="validate">Indicates if handler is validating</param>
		/// <param name="implementingType">Type that implements the handler</param>
		public void AddHttpHandler(string path, string verb, bool validate, Type implementingType)
		{
			if (string.IsNullOrEmpty(path) == true) throw new ArgumentNullException("path");
			if (string.IsNullOrEmpty(verb) == true) throw new ArgumentNullException("verb");
			if (implementingType == null) throw new ArgumentNullException("implementingType");

			Logger.L.AddInformation(Category.Configuration, "Adding HttpHandler: {0}", path);

			string n = string.Format(null, "add[@path={0}'][@type='{1}", path, implementingType.AssemblyQualifiedName);
			string x = "configuration/system.web/httpHandlers";
			string v = string.Format(null, "<add verb=\"{0}\" path=\"{1}\" validate=\"{2}\" type=\"{3}\" />", 
				verb, path, validate, implementingType.AssemblyQualifiedName);

			AddChildNode(n, x, v);

		}
		/// <summary>
		/// Adds a safecontrol entry to the list of modifications that will be applied
		/// </summary>
		/// <param name="implementingType">Type that implements the control</param>
		public void AddSafeControl(Type implementingType)
		{
			if (implementingType == null) throw new ArgumentNullException("implementingType");

			Logger.L.AddInformation(Category.Configuration, "Adding SafeControl: {0}", implementingType.Namespace);

			string n = string.Format(null, "SafeControl[@Namespace='{0}']", implementingType.Namespace);
			string x = "configuration/SharePoint/SafeControls";
			string v = string.Format(null, "<SafeControl Assembly=\"{0}\" Namespace=\"{1}\" TypeName=\"*\" Safe=\"True\" />",
				implementingType.Assembly.FullName, implementingType.Namespace);

			AddChildNode(n, x, v);
		}
		/// <summary>
		/// Adds an Expression builder entry to the list of modifications that will be applied
		/// </summary>
		/// <param name="implementingType">Type that implements the control</param>
		public void AddExpressionBuilder(string prefix, Type implementingType)
		{
			if (string.IsNullOrEmpty(prefix) == true) throw new ArgumentNullException("prefix");
			if (implementingType == null) throw new ArgumentNullException("implementingType");

			Logger.L.AddInformation(Category.Configuration, "Adding Expression: {0}", prefix);

			string n = string.Format(null, "add[@expressionPrefix='{0}'][@type='{1}']", prefix, implementingType.AssemblyQualifiedName); 
			string x = "configuration/system.web/compilation/expressionBuilders";
			string v = string.Format(null, "<add expressionPrefix=\"{0}\" type=\"{1}\" />",
				prefix, implementingType.AssemblyQualifiedName);

			AddChildNode(n, x, v);
		}
		/// <summary>
		/// Adds an appSetting entry to the list of modifications that will be applied
		/// </summary>
		/// <param name="key">Key of setting</param>
		/// <param name="value">Value of setting</param>
		public void AddAppSetting(string key, string value)
		{
			if (string.IsNullOrEmpty(key) == true) throw new ArgumentNullException("key");
			if (string.IsNullOrEmpty(value) == true) throw new ArgumentNullException("value");
			
			// make sur that the child node exists
			AddChildNode(
				string.Format(null, "add[@key='{0}']", key),
				"configuration/appSettings",
				string.Format(null, "<add key=\"{0}\" value=\"{1}\" />", key, value));

			// make sure that the value of the node is correct
			AddAttribute("value", string.Format(null, "configuration/appSettings/add[@key='{0}']", key), value);
		}
		/// <summary>
		/// Saves the changes
		/// </summary>
		public void Commit()
		{
			if (UpdateModifications() == true)
			{
				DateTime timeout = DateTime.Now.AddMinutes(5);
				// the SharePoint system only has one timer doing these types of updates
				// on a system where we have multiple web applications that require web config changes,
				// one update may not complete before the next one is initiated. This particularly happens on
				// multiple server installations (not local development)
				while (timeout > DateTime.Now)
				{
					try
					{
						_application.Farm.Services.GetValue<SPWebService>().ApplyWebConfigModifications();
						_application.Update();
						break;
					}
					catch (InvalidOperationException)
					{
						global::System.Threading.Thread.Sleep(1000);
					}
				}
				if (DateTime.Now > timeout)
				{
					Logger.L.AddError(Category.Configuration, 
						"Failed to update web.config {0}. Operation timed out", 
						_application.GetResponseUri(SPUrlZone.Default));
				}
			}
			else
			{
				Logger.L.AddInformation(Category.Configuration, "No changes applied to web config {0} since no changes have been made",
					_application.GetResponseUri(SPUrlZone.Default));
			}
		}
		/// <summary>
		/// Updates the SPWebApplication.WebConfigModifications with any changes
		/// </summary>
		/// <returns>True if there has been any modifications</returns>
		private bool UpdateModifications()
		{
			bool modified = false;

			// Remove any of the existing modifications that are not part of the existing set of modifications.
			// the _existingModifications was populated in the constructor
			foreach (SPWebConfigModification mod in _existingModifications)
			{
				if (FindModification(mod, _newModifications) == null)
				{
					_application.WebConfigModifications.Remove(mod);
					modified = true;
				}
			}
			// Add any new modifications that didn't already exist
			foreach (SPWebConfigModification mod in _newModifications)
			{
				if (FindModification(mod, _existingModifications) == null)
				{
					_application.WebConfigModifications.Add(mod);
					modified = true;
				}
			}
			return modified;
		}

		private static SPWebConfigModification FindModification(SPWebConfigModification mod, List<SPWebConfigModification> modifications)
		{
		    foreach (SPWebConfigModification m in modifications)
		    {
		        if ((m.Name.Equals(mod.Name) == true) &&
		            (m.Path.Equals(mod.Path) == true) &&
		            (m.Value.Equals(mod.Value) == true))
		        {
		            return m;
		        }
		    }
		    return null;
		}
	}
}
